Calcifer Calcifer 2 Calcifer 3 Calcifer 4
Teg

密码管理器 2password 安卓实现

2026/03/25 17:27 17 次阅读 王梓
打赏
✸ ✸ ✸

Android 密码管理器:安全存储与 MFA 实战

之前一篇文章《从1password到自建密码管理器:2password实战指南》,介绍了用 PyQt5 + MySQL 构建桌面版密码管理器的完整过程。桌面版运行稳定,但在实际使用中,手机端使用也很频繁。

于是我开始着手 2password 的 Android 版本,目标是实现与桌面版同等安全级别的移动端密码管理方案。

相比桌面版,Android 版密码管理器有其独特的优势:

  • 硬件级安全:利用 Android KeyStore 实现硬件级密钥保护,这是桌面端难以实现的
  • 生物识别集成:原生支持指纹和面容识别,解锁更便捷
  • 系统级自动锁定:利用应用生命周期实现自动锁定,安全无死角
  • 现代 UI 框架:Jetpack Compose 提供声明式 UI 开发体验,代码更简洁

今天这篇文章,我将分享如何构建一个功能完善的 Android 密码管理器。如果你读过我的 2password 文章,会发现这两个项目在设计理念上有很多相似之处,但在技术实现上又有各自的特点。先看看效果!

      

一、为什么从桌面版到移动版?

在开始之前,先聊聊为什么我要从桌面版的 2password 扩展到 Android 版本。

在《2password 实战指南》那篇文章中,我分享了一个基于 PyQt5 + MySQL 的桌面密码管理器。实际使用后,我发现了一些痛点:

  • 移动场景需求:很多时候我在手机上浏览网页,需要快速复制密码,但桌面版无法直接使用
  • 安全性限制:桌面版的密码存储在本地 MySQL,虽然实用但缺乏硬件级密钥保护
  • 生物识别缺失:桌面版没有原生生物识别支持,每次都要输入主密码略显繁琐

选择 Android 平台作为移动端方案,主要基于以下考虑:

  • 生态成熟:Jetpack Compose 提供现代化的 UI 开发体验,代码量比 PyQt5 更少
  • 安全框架:Android KeyStore 提供硬件级密钥保护,这是桌面端难以实现的安全特性
  • 生物识别:原生支持指纹和面容识别,解锁更便捷
  • 学习价值:从桌面到移动,是一次很好的技术栈拓展

两个版本的设计理念是一致的:简单实用、安全第一、数据自主。但在技术实现上,Android 版充分利用了平台特性,带来了更好的安全性和使用体验。

二、整体架构设计

密码管理器采用 MVVM 架构,结合 Jetpack Compose 构建 UI。整体架构如下:

Android 密码管理器整体架构 UI 层 (Jetpack Compose) HomeScreen - 密码列表 AddPasswordScreen - 添加密码 HistoryScreen - 历史记录 SettingsScreen - 设置 PasswordDetailScreen - 详情 EditPasswordScreen - 编辑 MasterPasswordScreen - 主密码 BatchOperationScreen - 批量操作 ViewModel 层 PasswordViewModel - 密码管理 AuthViewModel - 认证管理 StateFlow - 响应式数据流 Repository 层 PasswordRepository - 数据访问 Flow 异步数据流 加密/解密接口 数据库 (Room) • PasswordEntity - 密码表 • HistoryEntity - 历史表 • Migration - 数据迁移 安全层 • EncryptionManager - AES-256-GCM • Android KeyStore • TotpGenerator - MFA

三、核心功能模块

密码管理器的功能围绕"安全、便捷、可追溯"的原则设计。每个功能模块都经过精心打磨,确保既满足日常使用需求,又不牺牲安全性。

核心功能模块 密码管理 添加/编辑/删除 密码强度指示 显示/隐藏切换 MFA 生成 TOTP 验证码 30秒自动刷新 兼容Authenticator 历史追踪 操作记录 密码变更历史 永久保留 搜索过滤 实时搜索 分类筛选 收藏管理 导入导出 加密格式备份 数据恢复 防止数据丢失 安全特性 • Android KeyStore 硬件级密钥保护 • AES-256-GCM 加密 • IV 随机化防止重放攻击 • 生物识别解锁支持 • 自动锁定保护 • 应用备份禁用 数据流: 用户操作 → Compose UI → ViewModel → Repository → Room Database

四、密码添加流程

密码添加是一个涉及验证、加密、存储的完整流程。看似简单的"添加密码"操作,背后其实有一套严密的安全机制在运行。

 

密码添加流程 开始 点击添加按钮 打开添加对话框 填写信息 账号/密码/网站/MFA 验证必填 检查字段是否完整 通过? 显示错误 重新填写 AES-256-GCM加密 生成密文+IV 数据保存流程 1. EncryptionManager.encrypt(password) 2. 生成密文 ciphertext + IV 3. 组合: "ciphertext:iv" 4. PasswordEntity 保存到 Room 5. Flow 触发 UI 刷新 6. 关闭对话框

五、数据库设计

使用 Room 数据库,设计了两个核心表:密码表和历史表。每个密码都对应一个历史记录集合,这种设计确保了数据的完整性和可追溯性。

数据库 ER 图 PasswordEntity (密码表) id Long (主键) title String username String password String (AES-GCM加密) website String mfaSecret String (加密) HistoryEntity (历史表) id Long (主键) passwordId Long (外键) action String (添加/修改/删除) oldPassword String? (加密旧值) timestamp Long oldNotes String? (加密旧值) 1:N 索引优化 • passwords.id: 主键索引 • passwords.isFavorite: 收藏索引 • history.passwordId: 外键索引 • history.timestamp: 时间索引

 

六、安全加密实现

 

密码安全是核心功能。使用 Android KeyStore 生成并存储密钥,配合 AES-256-GCM 算法加密数据。这里有几个关键的安全细节值得注意。

 

object EncryptionManager {
    private const val KEY_ALIAS = "password_manager_key"
    private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
    private const val TRANSFORMATION = "AES/GCM/NoPadding"
    
    private val keyStore: KeyStore by lazy {
        KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) }
    }
    
    private fun createKey(): SecretKey {
        val keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES,
            KEYSTORE_PROVIDER
        )
        
        val spec = KeyGenParameterSpec.Builder(KEY_ALIAS,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setKeySize(256)
            .setRandomizedEncryptionRequired(true)  // 每次加密使用随机IV
            .build()
        
        return keyGenerator.generateKey()
    }
    
    fun encrypt(data: String): EncryptedData {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
        
        val encryptedBytes = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
        val iv = cipher.iv
        
        return EncryptedData(
            ciphertext = Base64.encodeToString(encryptedBytes, Base64.NO_WRAP),
            iv = Base64.encodeToString(iv, Base64.NO_WRAP)
        )
    }
}

AES-256-GCM 加密流程 Android KeyStore 硬件级密钥保护 密钥不可导出 获取 AES-256-GCM 加密算法 256位密钥 输出 加密结果 ciphertext:iv (组合字符串) 128位认证标签 安全特性 ✓ 密钥存储在硬件 KeyStore ✓ 每次加密生成随机 IV ✓ GCM 模式提供完整性校验 ✓ 防止密钥被提取 ✓ 防止重放攻击 ✓ 检测数据篡改 ✓ 允许备份 false ✓ 生物识别解锁 ✓ 自动锁定机制\

七、生物识别与自动锁定

为了提升安全性和便捷性,应用集成了生物识别和自动锁定功能:

@Singleton
class BiometricAuthManager @Inject constructor() {
    
    enum class BiometricStatus {
        AVAILABLE,
        NO_HARDWARE,
        HARDWARE_UNAVAILABLE,
        NONE_ENROLLED,
        SECURITY_UPDATE_REQUIRED
    }
    
    fun checkBiometricAvailability(context: Context): BiometricStatus {
        val biometricManager = BiometricManager.from(context)
        return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
            BiometricManager.BIOMETRIC_SUCCESS -> BiometricStatus.AVAILABLE
            BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> BiometricStatus.NO_HARDWARE
            BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> BiometricStatus.HARDWARE_UNAVAILABLE
            BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> BiometricStatus.NONE_ENROLLED
            else -> BiometricStatus.NO_HARDWARE
        }
    }
    
    suspend fun authenticate(
        activity: FragmentActivity,
        title: String = "验证身份",
        subtitle: String = "使用生物识别解锁密码管理器"
    ): Result<Boolean> = suspendCancellableCoroutine { continuation ->
        val biometricPrompt = BiometricPrompt(
            activity,
            ContextCompat.getMainExecutor(activity),
            object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    continuation.resume(Result.success(true))
                }
                
                override fun onAuthenticationFailed() {
                    continuation.resume(Result.success(false))
                }
                
                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    continuation.resume(Result.failure(BiometricAuthException(errorCode, errString.toString())))
                }
            }
        )
        
        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle(title)
            .setSubtitle(subtitle)
            .setNegativeButtonText("取消")
            .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
            .build()
        
        biometricPrompt.authenticate(promptInfo)
    }
}

生物识别流程 1. 检查硬件支持 BiometricManager canAuthenticate() 2. 提示用户认证 BiometricPrompt authenticate() 3. 认证结果回调 AuthenticationCallback onAuthenticationSucceeded 自动锁定机制 • 应用进入后台时自动锁定 • 用户可选择开启/关闭 • 保护应用免受未授权访问 • 生物识别成功后自动解锁 • 支持指纹和面容识别 • 设置持久化存储 • PreferencesManager 管理设置 • Hilt 依赖注入

八、MFA 验证码生成

支持 TOTP (Time-based One-Time Password) 算法,与 Google Authenticator 等应用完全兼容。这意味着你可以用一个密码管理器同时管理密码和 MFA,无需多个应用切换。

TOTP 的工作原理

TOTP 基于时间生成一次性密码,每 30 秒变化一次。服务端和客户端使用相同的密钥和当前时间计算验证码,只要时间同步,就能产生相同的验证码。RFC 6238 是这一算法的标准规范。

object TotpGenerator {
    private const val HMAC_SHA1 = "HmacSHA1"
    private const val DEFAULT_DIGITS = 6
    private const val DEFAULT_PERIOD = 30
    
    fun generateTotpCode(secret: String, digits: Int = DEFAULT_DIGITS): String {
        if (secret.isEmpty()) return ""
        
        val key = decodeBase32(secret)
        val time = System.currentTimeMillis() / 1000 / DEFAULT_PERIOD
        return generateOtp(key, time, digits)
    }
    
    private fun generateOtp(key: ByteArray, time: Long, digits: Int): String {
        val data = ByteBuffer.allocate(8).putLong(time).array()
        val signKey = SecretKeySpec(key, HMAC_SHA1)
        val mac = Mac.getInstance(HMAC_SHA1)
        mac.init(signKey)
        val hash = mac.doFinal(data)
        
        val offset = hash[hash.size - 1].toInt() and 0xf
        val truncatedHash = ((hash[offset].toInt() and 0x7f) shl 24) or
                           ((hash[offset + 1].toInt() and 0xff) shl 16) or
                           ((hash[offset + 2].toInt() and 0xff) shl 8) or
                           (hash[offset + 3].toInt() and 0xff)
        
        val otp = truncatedHash % (10.0.pow(digits.toDouble()).toInt())
        return otp.toString().padStart(digits, '0')
    }
}

TOTP 验证码生成流程 1. 解密 MFA 密钥 从 KeyStore 读取 Base32: JBSWY3DPEHPK3PXP 2. 获取当前时间 System.currentTimeMillis() 除以 30000 (30秒) 3. HMAC-SHA1 计算 密钥 + 时间步长 截取最后 6 位 123456 验证码 30秒有效 UI 自动刷新机制 1. Composable 中使用 LaunchedEffect 2. 启动 while(true) 循环 3. 每 1 秒调用 TotpGenerator 4. 更新 mfaCode 状态 5. recomposition 触发 UI 更新 6. 显示新的验证码 7. 用户复制验证码到剪贴板 • 内存开销: 低 • 刷新频率: 1秒 • 电池影响: 极小

九、历史记录追踪

所有密码修改都会自动记录历史,支持查看和恢复旧值。历史记录采用不可删除设计,确保审计完整性:

历史记录流程 修改前状态 password: OldPass123! username: old_user website: old-site.com notes: 旧备注 修改 修改后状态 password: NewPass456! username: new_user website: new-site.com notes: 新备注 历史记录 action: 修改 oldPassword: 加密旧值 oldUsername: old_user timestamp: 时间戳 历史特性: 不可删除 | 永久保留 | 支持密码显示/隐藏 | 按时间排序

十、数据导入导出

支持加密格式的数据导入导出,方便在设备间迁移,防止删除应用时数据丢失。

备份的重要性

再好的软件也可能出问题,手机也可能丢失或损坏。定期备份数据是保护自己资产的重要习惯。导出的数据文件也是加密的,即使文件被窃取也无法读取内容。

数据迁移流程 导出流程 1. 读取所有密码记录 2. 解密每个密码 3. 生成加密备份文件 4. 保存到本地存储 迁移 导入流程 1. 解析备份文件 2. 验证数据格式 3. 加密敏感字段 4. 批量插入数据库

十一、主题定制

为了提供个性化的使用体验,应用支持火主题:

private val LightColorScheme = lightColorScheme(
    primary = Color(0xFFD32F2F),           // 火主色
    primaryContainer = Color(0xFFFFE0E0),   // 浅红色容器
    secondary = Color(0xFFFF5722),         // 橙红色辅助色
    secondaryContainer = Color(0xFFFFCCBC), // 橙色容器
    tertiary = Color(0xFFE64A19),          // 深红色第三色
    background = Color(0xFFFFFBFE),        // 背景色
    surface = Color(0xFFFFFBFE),           // 表面色
    onPrimary = Color.White,               // 主色文字
    onSecondary = Color.White,             // 辅助色文字
    onTertiary = Color.White,             // 第三色文字
    onBackground = Color(0xFF1C1B1F),      // 背景文字
    onSurface = Color(0xFF1C1B1F)         // 表面文字
)

@Composable
fun PasswordManagerTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = false,  // 禁用 Material You
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    
    MaterialTheme(
        colorScheme = colorScheme,
        content = content
    )
}

关键点:设置 dynamicColor = false 禁用 Material You 动态颜色,确保自定义主题生效

十二、安全最佳实践

通过这个项目,我总结了一些移动端密码管理器开发的最佳实践。这些经验不仅适用于密码管理器,也适用于任何需要处理敏感数据的 Android 应用:

安全维度 推荐做法 本项目实现
密钥存储 使用 Android KeyStore 硬件保护 ✅ KeyStore + AES-256-GCM
加密算法 AES-256-GCM(认证加密) ✅ GCM 模式 + IV 随机化
数据传输 禁用应用备份 ✅ allowBackup=false
访问控制 生物识别 + 自动锁定 ✅ BiometricAuthManager
数据完整性 历史记录不可删除 ✅ 历史表无删除接口
MFA 保护 MFA 密钥加密存储 ✅ mfaSecret 字段加密
数据备份 提供导出备份功能 ✅ 加密格式导出/导入
主题保护 禁用动态颜色覆盖 ✅ dynamicColor=false

十三、Android 版 vs 桌面版对比

经过这次实践,我想对比一下两个版本的差异,帮助大家根据自己的需求选择合适的方案:

特性 2password (桌面版) Android 版
技术栈 Python + PyQt5 + MySQL Kotlin + Jetpack Compose + Room
数据存储 本地 MySQL 数据库 Room SQLite 数据库
加密方式 cryptography (AES-256) Android KeyStore + AES-256-GCM
生物识别 ❌ 无原生支持 ✅ 指纹/面容识别
自动锁定 ✅ 超时锁定 ✅ 后台自动锁定
MFA 支持 ✅ TOTP 生成 ✅ TOTP 生成
历史记录 ✅ 不可删除 ✅ 不可删除
数据迁移 ✅ CSV 导入导出 ✅ 加密格式导入导出
安全性 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ (硬件级保护)
便捷性 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ (生物识别)

我的使用体验

  • 桌面版适合:在电脑前长时间工作,需要批量管理密码的场景
  • Android 版适合:移动场景下的快速查看和复制密码,随时随地可用
  • 两个版本的数据是独立的,我目前同时使用,数据通过加密备份文件手动同步

 

十四、项目总结

这个自建的密码管理器完全满足我的日常需求:

  • - 本地存储,数据完全自主可控
  • - 硬件级加密,安全性有保障
  • - MFA 支持,与所有 TOTP 应用兼容
  • - 历史记录,所有操作可追溯
  • - 数据备份,防止删除应用时数据丢失
  • - 生物识别,便捷安全的解锁方式
  • - 自动锁定,保护应用免受未授权访问

如果你也在考虑自建密码管理器,不妨从 Android 平台开始。这是一个实用性强的项目,既能学到知识,又能解决实际问题。

最后提醒:密码安全无小事。无论使用哪种密码管理器,请确保:

使用强密码(长度至少 12 位,包含大小写、数字、特殊字符)

 

  • 为每个网站使用不同的密码
  • 启用 MFA 双因素认证
  • 定期备份数据
  • 不要将密码分享给他人

相关阅读

 

✸ ✸ ✸

📜 版权声明

本文作者:王梓 | 原文链接:https://www.bthlt.com/note/10015-密码管理器 2password 安卓实现

出处:葫芦的运维日志 | 转载请注明出处并保留原文链接

📜 留言板

留言提交后需管理员审核通过才会显示